/*
 * KeyValueCodingBase.java
 *
 * Created on November 5, 2006, 7:51 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package edu.umn.cs5115.scheduler.framework;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

/**
 * This class implements key-value coding methods to ease your implementation
 * of them.  It searches your subclass for accessor methods and will 
 * automatically call them when their key is requested.  Your setter should
 * call didChangeValueForKey after changing the value for a key in order to
 * send notifications.
 * @author grant
 */
public class KeyValueCodingBase implements KeyValueCoding
{
    /** Maps keys to a collection of KeyInfo's.  These KeyInfo's include 
     * collections of listeners on those keys for this object. */
    private HashMap keyInfos = new HashMap();
    
    // when notifications are being sent and somebody tries to register to 
    // receive notifications or remove themself as an observer, we can get a
    // ConcurrentModificationException.  In order to work around this, we need
    // to know when notifications are being sent and store removals and 
    // additions to be completed after the notification has finished.  The next
    // three maps deal with these problems.
    
    
    /** Maps keys to objects that forward notifications for those keys. 
     * This is used to forward notifications from ManagedSets to our own 
     * listeners.
     */
    private HashMap notificationForwarders = new HashMap();
    
    /** Number of times disableNotifications has been called, less number of 
     * times enableNotifications has been called.*/
    private int notificationDisabledLevel = 0;
    
    /**
     * Specify key dependencies for notifications.  Whenever a key in the 
     * collection keys is modified, a notification will also be sent out for the
     * dependent key.
     * Note:  It is your responsibility to ensure there are no circular 
     * dependancies.
     * @param objectClass The class of the subclass that is calling this method.
     * (ClassName.class)
     * @param keys A collection containing a list of keys that can trigger a
     * change for a dependent key.
     * @param dependentKey The dependent key for which a notification should be
     * triggered whenever one of the keys in the earlier collection is modified.
     */
    protected static void setKeysTriggerNotificationForDependentKey(Class objectClass, Collection keys, String dependentKey)
    {
        SubclassData.getSubclassDataForClass(objectClass).setKeysTriggerNotificationForDependentKey(keys, dependentKey);
    }
    
    /**
     * Add a listener for a given key.  If the listener already is receiving
     * notifications for the given key, calling this method has no effect. (the
     * listener will not be added twice)
     * @param listener The object to notify when the value of the key changes.
     * @param key The key to observe for changes.
     */
    public void addListener(KeyValueObserver listener, String key)
    {
        getKeyInfo(key).addListener(new ListenerInfo(listener));
    }

    /**
     * Remove a listener from a given key.  If the listener isn't listening to
     * the given key, calling this method does nothing.  Calling this method 
     * does not affect other keys the listener may be listening to, to remove a 
     * listener from all keys, call removeListener(KeyValueObserver).
     * 
     * @param listener The listener to remove from the list of objects to 
     * receive notifications when the value of the key changes.
     * @param key The key this listener no longer wants to observe for changes.
     * @see #removeListener(KeyValueObserver)
     */
    public void removeListener(KeyValueObserver listener, String key)
    {
        getKeyInfo(key).removeListener(new ListenerInfo(listener));
    }

    /**
     * Remove a listener so that it no longer receives notifications from this object.
     * If the specified listener isn't already listening to this object, this 
     * method has no effect.<p><b>Note:</b>If you know the key that this 
     * listener is listening to, it may be more efficient to call 
     * <code>removeListener(KeyValueObserver, String)</code> instead.
     * @param listener The listener to remove.
     * @see #removeListener(KeyValueObserver, String)
     */
    public void removeListener(KeyValueObserver listener)
    {
        // we need to remove this listener from every key that has ever been
        // listened to on this object.
        ListenerInfo listenerToRemove = new ListenerInfo(listener);
        Collection keyInfosList = keyInfos.values();
        Iterator infosIterator = keyInfosList.iterator();
        while(infosIterator.hasNext())
        {
            KeyInfo keyInfo = (KeyInfo) infosIterator.next();
            keyInfo.removeListener(listenerToRemove);
        }
    }

    /**
     * Gets the value for a key.  If this object does not declare a method for
     * getting that key, it is retrieved directly from the ManagedData storage.
     * If this object declares a method for getting
     * the specified key (for example, if the key is "keyName", and a method 
     * called "getKeyName" exists, or a method called "isKeyName" that returns
     * a boolean exists), then this method will be called as a result.  If the
     * getter throws an exception, it will be logged and ignored.  If you know
     * something about the class, you should use the getters directly rather
     * than this method so that you can catch exceptions thrown by the actual
     * getter.
     * @param key The key you want to get the value for.  This is usually a 
     * public string constant declared by the class you are getting a value 
     * from.
     */
    public Object getValueForKey(String key)
    {
        Method getter = SubclassData.getSubclassDataForClass(this.getClass()).getGetter(key);
        if (getter == null)
            return getValueForUndefinedKey(key);
        else {
            try
            {
                // this allows us to work with private classes, but only for
                // methods declared as public.  May want to change this to allow
                // protected/private methods to work, too, but I haven't decided
                // on that yet. (there's a similar trick in setValueForKey)
                if (Modifier.isPublic(getter.getModifiers()))
                    getter.setAccessible(true);
                return getter.invoke(this);
            } catch (IllegalArgumentException ex) {
                // shouldn't ever happen
                ex.printStackTrace();
            } catch (IllegalAccessException ex) {
                // I don't think this will happen...
                ex.printStackTrace();
            } catch (InvocationTargetException ex) {
                // stated behavior is to log the error and continue.  If 
                // somebody is using the generic interface, they probably won't
                // know how to deal with this problem any better than we do.
                ex.printStackTrace();
            }
            // return for exception paths
            return null;
        }
    }

    /**
     * Sets the value for a key.  If this object doesn't declare a method for
     * setting that key, it is retrieved directly from the ManagedData storage.
     * If this object declares a method for setting
     * the specified key (for example, if the key is "keyName", and a method 
     * called "setKeyName" exists in this class), then that method will be
     * called as a result.  If the existing setter throws an exception, it
     * is logged and ignored.  If you know something about the class, you should
     * use the setters directly rather than this method so that you can catch
     * exceptions thrown by the actual setter.  If the value is not of a proper
     * type, an exception is thrown.
     * @param key The key you want to set the value for.  This is usually a 
     * public string constant declared by the class you are setting this value
     * in.
     * @param value The value you want to set this key to.  If the class has 
     * custom setters, you must take care that value is an object of the type 
     * returned by getKeyName.  (or it's java wrapper class, if the type is a 
     * primitive).
     * @throws IllegalArgumentException if value is of the wrong type and cannot
     * be converted to the correct type.
     * (note that no attempt at conversion is made at present...this will likely
     * change in the future for convenience)
     */
    public void setValueForKey(final String key, Object value)
    {
        Method setter = SubclassData.getSubclassDataForClass(this.getClass()).getSetter(key);
        if (setter == null)
            setValueForUndefinedKey(key, value);
        else
        {
            try
            {
                Object convertedValue = convertValueToType(value, setter.getParameterTypes()[0]);
                // this allows us to work with private classes, but only for
                // methods declared as public.  May want to change this to allow
                // protected/private methods to work, too, but I haven't decided
                // on that yet. (there's a similar trick in getValueForKey)
                if (Modifier.isPublic(setter.getModifiers()))
                    setter.setAccessible(true);
                setter.invoke(this, convertedValue);
            } catch (InvocationTargetException ex) {
                ex.printStackTrace();
            } catch (IllegalAccessException ex) {
                ex.printStackTrace();
            }
        }
    }
    
    /**
     * Try to convert a value to a given type.
     * @param value A generic object that may not be of the correct type.
     * @param type The desired type for the object.
     * @return A converted value for this object, if a conversion was possible
     * and necessary.  Otherwise, it returns the original value itself
     */
    private Object convertValueToType(Object value, Class type)
    {
        if (type.isInstance(value))
            return value;
        Object convertedValue = value;
        if (type == String.class)
        {
            if (value == null)
                return "";
            else
                convertedValue = value.toString();
            
        } else if (type == Double.class || type == double.class) {
            convertedValue = convertValueToNumber(value).doubleValue();
        } else if (type == Float.class || type == float.class) {
            convertedValue = convertValueToNumber(value).floatValue();
        } else if (type == Long.class || type == long.class) {
            convertedValue = convertValueToNumber(value).longValue();
        } else if (type == Integer.class || type == int.class) {
            convertedValue = convertValueToNumber(value).intValue();
        } else if (type == Short.class || type == short.class) {
            convertedValue = convertValueToNumber(value).shortValue();
        } else if (type == Byte.class || type == byte.class) {
            convertedValue = convertValueToNumber(value).byteValue();
        } else if (type == Boolean.class || type == boolean.class) {
            if (value instanceof Boolean)
                return value;
            convertedValue = (convertValueToNumber(value).doubleValue() != 0);
        } else if (type == Character.class || type == char.class) {
            if (value instanceof String)
                return ((String)value).charAt(0);
            else
                return (char)(convertValueToNumber(value).shortValue());
        }
        return convertedValue;
    }
    
    /**
     * Trys to turn this object into a number.
     * @param value An object to convert
     * @return A Number containing some representation of the original value.
     */
    private Number convertValueToNumber(Object value)
    {
        if (value == null)
            return 0;
        if (value instanceof Number)
            return (Number) value;
        else {
            try
            {
                return Double.valueOf(value.toString());
            } catch (NumberFormatException e) {
                return 0;
            }
        }
    }
    
    /**
     * What should be done when the key cannot be found?  Subclasses should 
     * override this method to provide custom behavior.
     * @param key The key that was requested, but that couldn't be found.
     * @return The object to be returned when the key can't be found.  Default
     * behavior is to return null.
     */
    protected Object getValueForUndefinedKey(String key)
    {
        System.out.println("Requested undefined key \"" + key + "\" of object " + this);
        return null;
    }
    
    /**
     * What should be done when the key to set cannot be found?  Subclasses can
     * override this method to provide custom behavior.
     * @param key They key that was requested, but that couldn't be found.
     * @param value The value the key was supposed to be set to.
     */
    protected void setValueForUndefinedKey(String key, Object value)
    {
        String objectValue;
        if (value == null)
            objectValue = "(null)";
        else
            objectValue = value.toString();
        System.out.println("Set undefined key \"" + key + "\" of object " + this + " to " + objectValue);
    }
    
    /** 
     * Called after a key changes.  If you want to support notifications without
     * using the data storage and undo support provided by ManagedData, you can
     * call this method directly.  Normally, it is not necessary to do so, and
     * doing so will result in duplicate notifications being sent.  Only call
     * this method if you do not call setPrimitiveValueForKey in your setter.
     * @param key The key which has changed, and for which listeners should be
     * notified of the availability of a new value.
     */
    protected void didChangeValueForKey(String key)
    {
        // regardless of whether or not we change the value, we need to get the
        // value and see if it is a set.  If it is, we'll need to set up a
        // forwarder.  If it's not, we'll remove the forwarder if it's present
        Object value = getValueForKey(key);
        if (value instanceof KeyValueCodingSet)
        {
            SetNotificationForwarder forwarder = (SetNotificationForwarder) notificationForwarders.get(key);
            if (forwarder == null)
            {
                forwarder = new SetNotificationForwarder((KeyValueCodingSet)value, key);
                notificationForwarders.put(key, forwarder);
            } else
                forwarder.changeObservedSet((KeyValueCodingSet)value);
        } else {
            SetNotificationForwarder forwarder = (SetNotificationForwarder) notificationForwarders.get(key);
            if (forwarder != null)
                forwarder.changeObservedSet(null);
        }
        
        if (notificationDisabledLevel <= 0)
        {
            KeyInfo info = getKeyInfo(key);
            info.willSendNotifications();
            Collection listenersForThisKey = info.getListeners();
            Iterator iterator = listenersForThisKey.iterator();
            while(iterator.hasNext())
                ((ListenerInfo)iterator.next()).listener.valueChanged(this, key);
            info.didSendNotifications();
            
            SubclassData data = SubclassData.getSubclassDataForClass(this.getClass());
            Collection dependentKeys = data.getDependentKeysForKey(key);
            if (dependentKeys != null)
            {
                iterator = dependentKeys.iterator();
                while(iterator.hasNext())
                    didChangeValueForKey((String)iterator.next());
            }
        }
    }
    
    /**
     * Called after the set value for a key changes.
     * @param key The key which has changed, and for which listeners should be
     * notified of the availability of a new value.
     * @param mutationType The type of mutation that was used to change the key.
     * @param objects The objects that were used to perform the mutation.
     */
    protected void didChangeValueForKey(String key, KeyValueObserver.SetMutationType mutationType, Set objects)
    {
        if (notificationDisabledLevel <= 0)
        {
            KeyInfo info = getKeyInfo(key);
            info.willSendNotifications();
            Collection listenersForThisKey = info.getListeners();
            Iterator iterator = listenersForThisKey.iterator();
            while(iterator.hasNext())
                ((ListenerInfo)iterator.next()).listener.valueChanged(this, key, mutationType, objects);
            info.didSendNotifications();
            
            SubclassData data = SubclassData.getSubclassDataForClass(this.getClass());
            Collection dependentKeys = data.getDependentKeysForKey(key);
            if (dependentKeys != null)
            {
                iterator = dependentKeys.iterator();
                while(iterator.hasNext())
                    didChangeValueForKey((String)iterator.next());
            }
        }
    }
    
    /** 
     * Temporarily disable notifications for this object. You may call this 
     * function multiple times, but to re-enable notifications you must call
     * enableNotifications once for each call to disableNotifications.
     * @see #enableNotifications()
     */
    public void disableNotifications()
    {
        notificationDisabledLevel++;
    }
    
    /**
     * Re-enable notifications for this object.  Notifications are enabled by
     * default, so you should only call this to undo the effects of an earlier
     * call to disableNotifications.  This method must be called once for every
     * call to disableNotifications before notifications are re-enabled.
     * @throws IllegalStateException if you attempt to enable notifications 
     * while they are not disabled.
     * @see #disableNotifications()
     */
    public void enableNotifications()
    {
        if (notificationDisabledLevel > 0)
            notificationDisabledLevel--;
        else
            throw new IllegalStateException("Attempt to enable notifications while they were not disabled.");
    }
    
    /**
     * Returns the key info for the given key, creating it if necessary.
     * @return The key info for the given key.  If this is the first time the
     * method is called, this is a new instance of KeyInfo.  In future calls to
     * getKeyInfo with the same key, the same instance will be returned.
     */
    private KeyInfo getKeyInfo(String key)
    {
        KeyInfo info = (KeyInfo) keyInfos.get(key);
        if (info == null)
        {
            info = new KeyInfo();
            keyInfos.put(key, info);
        }
        return info;
    }
    
    /**
     * Holds information about a key that we notify on.  This includes the
     * listeners registered to it, whether or not a notification is being sent
     * for this key, and such.
     * TODO: Make this use the new 
     */
    private class KeyInfo
    {
        /** List of listeners currently listening to this key. */
        private Collection listeners = new ArrayList();
        /** Listeners that were added while the key was having a notification sent on it.*/
        private Collection addedListeners = new ArrayList();
        /** Listeners that were removed while the key was having a notification sent. */
        private Collection removedListeners = new ArrayList();
        /** number of times notifications have been recursively started on this key. */
        private int notificationLevel;
        
        /**
         * Add a listener to the list of listeners for this key.  If there is a
         * notification in progress, the listner will not be added until there
         * are no more notifications being sent on this key.
         * @param listener The listener to add.
         */
        public void addListener(ListenerInfo listener)
        {
            if (notificationLevel <= 0)
            {
                if (!listeners.contains(listener))
                    listeners.add(listener);
            } else {
                if (removedListeners.contains(listener))
                    removedListeners.remove(listener);
                if (!addedListeners.contains(listener))
                    addedListeners.add(listener);
            }
        }
        
        /**
         * Remove a listener from the list of listeners for this key.  If there
         * is a notification in progress, the listener will not be removed until
         * there are no more notifications being sent on this key.
         * @param listener The listener to (eventually) remove.
         */
        public void removeListener(ListenerInfo listener)
        {
            if (notificationLevel <= 0)
            {
                listeners.remove(listener);
            } else {
                if (addedListeners.contains(listener))
                    addedListeners.remove(listener);
                if (!removedListeners.contains(listener))
                    removedListeners.add(listener);
            }
        }
        
        /**
         * Get the current list of listeners.
         * @return A list of listeners that are listening to this key.  This is
         * never null, even if there are no registered listeners.
         */
        public Collection getListeners()
        {
            return listeners;
        }
        
        /**
         * Call this method before calling getListeners() to send notifications
         * for a key.  Once you have called this method, match it with 
         * didSendNotifications().  The number of calls must be equivalent.
         * @see didSendNotifications
         */
        public void willSendNotifications()
        {
            notificationLevel++;
        }
        
        public void didSendNotifications()
        {
            if (notificationLevel > 0)
            {
                notificationLevel--;
                if (notificationLevel <= 0)
                {
                    listeners.addAll(addedListeners);
                    listeners.removeAll(removedListeners);
                }
            } else {
                throw new IllegalStateException("didSendNotifications called without matching call to willSendNotifications");
            }
        }
    }
    
    /** Holds information about a listener.  Right now, this is actuall just
     * the listener, but in the future, other options could be added here. */
    private static class ListenerInfo
    {
        /** The object listening to a value that this ListenerInfo is for. */
        public KeyValueObserver listener;
        
        /** Create a new instance of ListenerInfo for a specific listener.
         * @param listener The listener associated with this information.
         */
        public ListenerInfo(KeyValueObserver listener)
        {
            this.listener = listener;
        }
        
        public int hashCode()
        {
            return listener.hashCode();
        }

        public boolean equals(Object obj)
        {
            if (obj instanceof ListenerInfo)
            {
                return ((ListenerInfo)obj).listener == listener;
            }
            return false;
        }
    }
    
    /**
     * Store information about known subclasses.  This includes method lookups
     * that are cached for efficiency, as well as lists of dependent keys for
     * triggering notifications.
     */
    private static class SubclassData
    {
        /** Stores a list of known subclasses that have been retrieved using
         * getSubclassDataForClass().
         */
        private static HashMap knownSubclasses = new HashMap();
    
        /** This is used to represent null in the map to avoid having
         * to call both contains() and get() */
        private final static Object NULL = new Object();
        
        /** The class we contain data for. */
        private Class subclass;
        
        /** List of methods we've already constructed, looked up, and stored.
         * This maps string keys to their corresponding Method objects. */
        private HashMap methodList = new HashMap();
        
        /** This tells us what other notifications to trigger when a key is 
         * changed.  It maps keys to a collection of dependent keys. */
        private HashMap dependentKeys = new HashMap();
        
        /**
         * Creates a new instance of SubclassData for the class subclass.
         * Don't call this, always use getSubclassDataForClass instead.
         * @param subclass The class to store data for.
         */
        public SubclassData(Class subclass)
        {
            this.subclass = subclass;
            
            // find and cache the list of setters and getters in this class.
            buildMethodList();
        }
        
        /**
         * Get the SubclassData for a given class.  If this is the first time 
         * this method is called, a new object is created and returned.  
         * Subsequent calls will return the same object.
         * @param subclass The class to get data for.  No attempt is made to 
         * enforce that this is actually a subclass of ManagedData.
         * @return The SubclassData for the given class.
         */
        public static SubclassData getSubclassDataForClass(Class subclass)
        {
            SubclassData data = (SubclassData) knownSubclasses.get(subclass);
            if (data == null)
            {
                data = new SubclassData(subclass);
                knownSubclasses.put(subclass, data);
            }
            return data;
        }
        
        /**
         * Specify key dependencies for notifications.  Whenever a key in the 
         * collection keys is modified, a notification will also be sent out for
         * the dependent key.
         * @param keys A collection containing a list of keys that can trigger a
         * change for a dependent key.
         * @param dependentKey The dependent key for which a notification should
         * be triggered whenever one of the keys in the earlier collection is 
         * modified.
         */
        public void setKeysTriggerNotificationForDependentKey(Collection keys, String dependentKey)
        {
            Iterator iterator = keys.iterator();
            while (iterator.hasNext())
            {
                String thisKey = (String) iterator.next();
                Collection dependentKeysForThisKey = (Collection) dependentKeys.get(thisKey);
                if (dependentKeysForThisKey == null)
                {
                    dependentKeysForThisKey = new ArrayList();
                    dependentKeys.put(thisKey, dependentKeysForThisKey);
                }
                dependentKeysForThisKey.add(dependentKey);
            }
        }
        
        /**
         * Get a collection of dependent keys for a given key.  When the key
         * changes, all of the keys in the returned collection, in addition to
         * the key that changed, should have notifications sent.
         * @param key The key to find dependent keys for
         * @return A collection of keys that should also have notifications 
         * triggered when the other key is changed.  If there are no dependent
         * keys, null is returned.
         */
        public Collection getDependentKeysForKey(String key)
        {
            return (Collection) dependentKeys.get(key);
        }
        
        /**
         * Gets the setter method for a given key.  This works by, for a key 
         * named "keyName", looking for a method named "setKeyName".
         * @param key The key to search for a setter for.
         * @return A Method that can be invoked to set the given key's value.  
         * If no setter was found, this method returns null.
         * @throws IllegalArgumentException if key is null or the empty string.
         */
        public Method getSetter(String key) throws IllegalArgumentException
        {
            if (key == null || key.length() == 0)
                throw new IllegalArgumentException("Key must be a non-empty non-null string.");
            Accessors accessors = (Accessors) methodList.get(key);
            if (accessors == null)
                return null;
            else
                return accessors.setter;
        }
        
        /**
         * Gets the getter method for a given key.  This works by searching for
         * a method named appropriately given the key name.  For example, if the
         * key name is "keyName", this searches for a method named "getKeyName"
         * with any return value, or a method named "isKeyName" that returns
         * a boolean or a Boolean.
         * @param key The key to get the getter for.
         * @param A Method that can be invoked to return the key's value.
         * If no getter was found, this method returns null.
         * @throws IllegalArgumentException if key is null or the empty string.
         */
        public Method getGetter(String key) throws IllegalArgumentException
        {
            if (key == null || key.length() == 0)
                throw new IllegalArgumentException("Key must be a non-empty non-null string.");
            Accessors accessors = (Accessors) methodList.get(key);
            if (accessors == null)
                return null;
            else
                return accessors.getter;
        }

        /**
         * Scans the names of all of the methods in this class.  If the method
         * begins with "set", "get", or "is", this method strips out the 
         * appropriate key.<p>
         * In the case that the method is named something like, 
         * "getHTMLProperties", or "getALife", both the keys "HTMLProperties"
         * and "hTMLProperties" as well as "ALife" and "aLife" will be 
         * associated with the methods.
         * If multiple getters are found, the one with the no parameters and
         * a return value is selected.  If no such getters exist, no getter is
         * saved.
         * If multiple setters are found, the one with one parameter will be 
         * selected.  If multiple setters of this type exist, the one with the 
         * return type matching the getter is selected.  If there are no exact 
         * matches, an arbitrary setter is chosen.
         */
        private void buildMethodList()
        {
            Method[] methods = subclass.getMethods();
            for(int i = 0; i < methods.length; i++)
            {
                Method thisMethod = methods[i];
                String methodName = thisMethod.getName();
                String keyName = extractKeyName(methodName);
                if (keyName != null)
                {
                    Accessors accessors = (Accessors) methodList.get(keyName);
                    if (accessors == null)
                    {
                        accessors = new Accessors();
                        if (isSetter(thisMethod))
                        {
                            accessors.setter = thisMethod;
                        } else if (isGetter(thisMethod)) {
                            accessors.getter = thisMethod;
                        }
                        if (accessors.getter != null || accessors.setter != null)
                        {
                            methodList.put(keyName, accessors);
                            String alternateKey = getAlternateKeyName(keyName);
                            if (alternateKey != null)
                                methodList.put(alternateKey, accessors);
                        }
                    } else {
                        if (isSetter(thisMethod))
                        {
                            if (accessors.setter == null)
                                accessors.setter = thisMethod;
                            else if (accessors.getter != null && accessors.getter.getReturnType() == thisMethod.getParameterTypes()[0])
                                accessors.setter = thisMethod;
                        }
                        if (isGetter(thisMethod))
                        {
                            if (accessors.getter == null)
                                accessors.getter = thisMethod;
                            else // shouldn't ever be an else
                                System.err.println("Duplicate valid getter found!");
                        }
                    }
                }
            }
        }
        
        /**
         * Gets a key name from an accessor method's name.  The accessor method
         * may be a set, get, or is method.  The first letter of the key in the
         * method name should be capitalized.  It will be converted to lower 
         * case before being returned.
         * @param An accessor name.
         * @return The name of the key the accessor accesses.  If the method is
         * not "get", "set", or "is" followed by a capital letter, null is 
         * returned.
         * @throws NullPointerException if accessorName is null.
         */
        private String extractKeyName(String accessorName)
        {
            if (accessorName.length() > 2)
            {
                if (accessorName.startsWith("is") && Character.isUpperCase(accessorName.charAt(2)))
                {
                    return Character.toLowerCase(accessorName.charAt(2)) + accessorName.substring(3);
                }
            }
            if (accessorName.length() > 3)
            {
                if ((accessorName.startsWith("get") || accessorName.startsWith("set")) && Character.isUpperCase(accessorName.charAt(3)))
                {
                    return Character.toLowerCase(accessorName.charAt(3)) + accessorName.substring(4);
                }
            }
            return null;
        }
        
        /**
         * Gets the alternate key name for the given key.  For example, if the
         * key name is hTMLProperties, the alternate key name is HTMLProperties.
         * If the key name is forgottenThings, there is no alternate name.
         * @param keyName The standard keyName returned by extractKeyName
         * @return An alternate key name, in which the first letter is 
         * capitalized if the second letter is capitalized.  If the second
         * letter is not capitalized, this method returns null.
         * @throws NullPointerException if keyName is null.
         */
        private String getAlternateKeyName(String keyName)
        {
            if (keyName.length() > 1)
                if (Character.isUpperCase(keyName.charAt(1)))
                    return Character.toUpperCase(keyName.charAt(0))+keyName.substring(1);
            return null;
        }
        
        /**
         * Test whether a method is a setter.
         * @param method A method to test.
         * @return True if it is a setter (starts with "set" and has exactly 1
         * parameter), false otherwise.
         */
        private boolean isSetter(Method method)
        {
            return method.getName().startsWith("set") && (method.getParameterTypes().length == 1);
        }
        
        /**
         * Tests whether a method is a getter.
         * @param method A method to test.
         * @return True if it is a getter.  This means that it starts with "get"
         * or "is", and has a non-void return type, and no parameters.  In the
         * case that it starts with "is", the return type must be boolean or
         * Boolean.
         */
        private boolean isGetter(Method method)
        {
            Class returnType = method.getReturnType();
            return ((method.getName().startsWith("is") && 
                     (returnType == boolean.class || returnType == Boolean.class)) ||
                    (method.getName().startsWith("get") &&
                     (returnType != void.class))) &&
                   method.getParameterTypes().length == 0;
        }
        
        /** Holds the accessors for a particular key. */
        private static class Accessors
        {
            /** The getter for this set of accessors. */
            public Method getter = null;
            /** The setter for this set of accessors. */
            public Method setter = null;
        }
    }
    
    /** Class to forward notifications from a ManagedSet. */
    private class SetNotificationForwarder extends KeyValueAdapter
    {
        /** The set we are currently observing. */
        private KeyValueCodingSet observedSet;
        /** The key for this set. */
        private String key;
        
        /**
         * Create a new instance of SetNotificationForwarder.
         * @param observedSet The set to observe.
         * @param key The key that we should forward notifications to.
         */
        public SetNotificationForwarder(KeyValueCodingSet observedSet, String key)
        {
            this.key = key;
            changeObservedSet(observedSet);
        }
        
        /**
         * Change the set that is currently being removed.  This removes this 
         * object as an observer from the current set and adds it instead to
         * the new set.
         */
        public void changeObservedSet(KeyValueCodingSet newObservedSet)
        {
            if (observedSet != null)
                observedSet.removeListener(this, ManagedSet.SET_CHANGE_NOTIFICATION_KEY);
            this.observedSet = newObservedSet;
            if (newObservedSet != null)
                newObservedSet.addListener(this, ManagedSet.SET_CHANGE_NOTIFICATION_KEY);
        }
        
        /** 
         * Forwards notifications from the set.
         * @param object This should always be our observedSet.
         * @param mutation The type of mutation to forward.
         * @param modifyingSet The objects used to modify the set to forward.
         */
        public void valueChanged(KeyValueCoding object, String key, SetMutationType mutation, Set modifyingSet)
        {
            didChangeValueForKey(this.key, mutation, modifyingSet);
        }
    }
}
